// CHECKSTYLE_OFF: Copyrighted to ASF
/*
 * Copyright 1999-2004 The Apache Software Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
// CHECKSTYLE_ON
package com.google.j2cl.jre.java.util;

import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
 * Tests base {@link java.util.Map} methods and contracts.
 * <p>
 * The forces at work here are similar to those in {@link TestCollection}.
 * If your class implements the full Map interface, including optional
 * operations, simply extend this class, and implement the {@link
 * #makeEmptyMap()} method.
 * <p>
 * On the other hand, if your map implemenation is weird, you may have to
 * override one or more of the other protected methods.  They're described
 * below.<P>
 *
 * <B>Entry Population Methods</B><P>
 *
 * Override these methods if your map requires special entries:
 *
 * <UL>
 * <LI>{@link #getSampleKeys()}
 * <LI>{@link #getSampleValues()}
 * <LI>{@link #getNewSampleValues()}
 * <LI>{@link #getOtherKeys()}
 * <LI>{@link #getOtherValues()}
 * </UL>
 *
 * <B>Supported Operation Methods</B><P>
 *
 * Override these methods if your map doesn't support certain operations:
 *
 * <UL>
 * <LI> {@link #useDuplicateValues()}
 * <LI> {@link #useNullKey()}
 * <LI> {@link #useNullValue()}
 * <LI> {@link #isAddRemoveModifiable()}
 * <LI> {@link #isChangeable()}
 * </UL>
 *
 * <B>Fixture Methods</B><P>
 *
 * For tests on modification operations (puts and removes), fixtures are used
 * to verify that that operation results in correct state for the map and its
 * collection views.  Basically, the modification is performed against your
 * map implementation, and an identical modification is performed against
 * a <I>confirmed</I> map implementation.  A confirmed map implementation is
 * something like <Code>java.util.HashMap</Code>, which is known to conform
 * exactly to the {@link Map} contract.  After the modification takes place
 * on both your map implementation and the confirmed map implementation, the
 * two maps are compared to see if their state is identical.  The comparison
 * also compares the collection views to make sure they're still the same.<P>
 *
 * The upshot of all that is that <I>any</I> test that modifies the map in
 * <I>any</I> way will verify that <I>all</I> of the map's state is still
 * correct, including the state of its collection views.  So for instance
 * if a key is removed by the map's key set's iterator, then the entry set
 * is checked to make sure the key/value pair no longer appears.<P>
 *
 * The {@link #map} field holds an instance of your collection implementation.
 * The {@link #entrySet}, {@link #keySet} and {@link #collectionValues} fields hold
 * that map's collection views.  And the {@link #confirmed} field holds
 * an instance of the confirmed collection implementation.  The
 * {@link #resetEmpty()} and {@link #resetFull()} methods set these fields to
 * empty or full maps, so that tests can proceed from a known state.<P>
 *
 * After a modification operation to both {@link #map} and {@link #confirmed},
 * the {@link #verify()} method is invoked to compare the results.  The {@link
 * verify()} method calls separate methods to verify the map and its three
 * collection views ({@link verifyMap(), {@link verifyEntrySet()}, {@link
 * verifyKeySet()}, and {@link verifyValues()}).  You may want to override one
 * of the verification methodsto perform additional verifications.  For
 * instance, {@link TestDoubleOrderedMap} would want override its {@link
 * #verifyValues()} method to verify that the values are unique and in
 * ascending order.<P>
 *
 * <B>Other Notes</B><P>
 *
 * If your {@link Map} fails one of these tests by design, you may still use
 * this base set of cases.  Simply override the test case (method) your {@link
 * Map} fails and/or the methods that define the assumptions used by the test
 * cases.  For example, if your map does not allow duplicate values, override
 * {@link #useDuplicateValues()} and have it return <code>false</code>
 */
@SuppressWarnings({"unchecked", "rawtypes"})
@NullMarked
abstract class TestMap extends TestObject {

  // These instance variables are initialized with the reset method.
  // Tests for map methods that alter the map (put, putAll, remove)
  // first call reset() to create the map and its views; then perform
  // the modification on the map; perform the same modification on the
  // confirmed; and then call verify() to ensure that the map is equal
  // to the confirmed, that the already-constructed collection views
  // are still equal to the confirmed's collection views.

  /** Map created by reset(). */
  protected Map<@Nullable Object, @Nullable Object> map;

  /** Entry set of map created by reset(). */
  protected Set<Map.Entry<@Nullable Object, @Nullable Object>> entrySet;

  /** Key set of map created by reset(). */
  protected Set<@Nullable Object> keySet;

  /** Values collection of map created by reset(). */
  protected Collection<@Nullable Object> collectionValues;

  /** HashMap created by reset(). */
  protected Map<@Nullable Object, @Nullable Object> confirmed;

  /**
   * Override if your map does not allow a <code>null</code> key. The default implementation returns
   * <code>true</code>
   */
  protected boolean useNullKey() {
    return true;
  }

  /**
   * Override if your map does not allow <code>null</code> values. The default implementation
   * returns <code>true</code>.
   */
  protected boolean useNullValue() {
    return true;
  }

  /**
   * Override if your map does not allow duplicate values. The default implementation returns <code>
   * true</code>.
   */
  protected boolean useDuplicateValues() {
    return true;
  }

  /**
   * Override if your map allows its mappings to be changed to new values. The default
   * implementation returns <code>true</code>.
   */
  protected boolean isChangeable() {
    return true;
  }

  /**
   * Override if your map does not allow add/remove modifications. The default implementation
   * returns <code>true</code>.
   */
  protected boolean isAddRemoveModifiable() {
    return true;
  }

  /**
   * Override if your map allows concurrent modifications. The default implementation returns <code>
   * true</code>.
   */
  protected boolean isFailFastExpected() {
    return true;
  }

  /**
   * Returns the set of keys in the mappings used to test the map. This method must return an array
   * with the same length as {@link #getSampleValues()} and all array elements must be different.
   * The default implementation constructs a set of String keys, and includes a single null key if
   * {@link #useNullKey()} returns <code>true</code>.
   */
  protected @Nullable Object[] getSampleKeys() {
    @Nullable Object[] result =
        new @Nullable Object[] {
          "blah",
          "foo",
          "bar",
          "baz",
          "tmp",
          "gosh",
          "golly",
          "gee",
          "hello",
          "goodbye",
          "we'll",
          "see",
          "you",
          "all",
          "again",
          "key",
          "key2",
          useNullKey() ? null : "nonnullkey"
        };
    return result;
  }

  protected Object[] getOtherKeys() {
    return TestCollection.getOtherNonNullStringElements();
  }

  protected Object[] getOtherValues() {
    return TestCollection.getOtherNonNullStringElements();
  }

  /**
   * Returns the set of values in the mappings used to test the map. This method must return an
   * array with the same length as {@link #getSampleKeys()}. The default implementation contructs a
   * set of String values and includes a single null value if {@link #useNullValue()} returns <code>
   * true</code>, and includes two values that are the same if {@link #useDuplicateValues()} returns
   * <code>true</code>.
   */
  protected @Nullable Object[] getSampleValues() {
    @Nullable Object[] result =
        new @Nullable Object[] {
          "blahv",
          "foov",
          "barv",
          "bazv",
          "tmpv",
          "goshv",
          "gollyv",
          "geev",
          "hellov",
          "goodbyev",
          "we'llv",
          "seev",
          "youv",
          "allv",
          "againv",
          useNullValue() ? null : "nonnullvalue",
          "value",
          useDuplicateValues() ? "value" : "value2",
        };
    return result;
  }

  /**
   * Returns a the set of values that can be used to replace the values returned from {@link
   * #getSampleValues()}. This method must return an array with the same length as {@link
   * #getSampleValues()}. The values returned from this method should not be the same as those
   * returned from {@link #getSampleValues()}. The default implementation constructs a set of String
   * values and includes a single null value if {@link #useNullValue()} returns <code>true</code>,
   * and includes two values that are the same if {@link #useDuplicateValues()} returns <code>true
   * </code>.
   */
  protected @Nullable Object[] getNewSampleValues() {
    @Nullable Object[] result =
        new @Nullable Object[] {
          useNullValue() ? null : "newnonnullvalue",
          "newvalue",
          useDuplicateValues() ? "newvalue" : "newvalue2",
          "newblahv",
          "newfoov",
          "newbarv",
          "newbazv",
          "newtmpv",
          "newgoshv",
          "newgollyv",
          "newgeev",
          "newhellov",
          "newgoodbyev",
          "newwe'llv",
          "newseev",
          "newyouv",
          "newallv",
          "newagainv",
        };
    return result;
  }

  /**
   * Helper method to add all the mappings described by {@link #getSampleKeys()} and {@link
   * #getSampleValues()}.
   */
  protected void addSampleMappings(Map m) {

    @Nullable Object[] keys = getSampleKeys();
    @Nullable Object[] values = getSampleValues();

    for (int i = 0; i < keys.length; i++) {
      try {
        m.put(keys[i], values[i]);
      } catch (NullPointerException exception) {
        assertTrue(
            "NullPointerException only allowed to be thrown "
                + "if either the key or value is null.",
            keys[i] == null || values[i] == null);

        if (keys[i] == null) {
          if (useNullKey()) {
            throw new Error(
                "NullPointerException on null key, but "
                    + "useNullKey is not overridden to return false.",
                exception);
          }
        } else if (values[i] == null) {
          if (useNullValue()) {
            throw new Error(
                "NullPointerException on null value, but "
                    + "useNullValue is not overridden to return false.",
                exception);
          }
        } else {
          // Unknown reason for NullPointer.
          throw exception;
        }
      }
    }
    assertEquals("size must reflect number of mappings added.", keys.length, m.size());
  }

  /** Return a new, empty {@link Map} to be used for testing. */
  protected abstract Map<@Nullable Object, @Nullable Object> makeEmptyMap();

  protected Map<@Nullable Object, @Nullable Object> makeConfirmedMap() {
    return new HashMap();
  }

  /**
   * Return a new, populated map. The mappings in the map should match the keys and values returned
   * from {@link #getSampleKeys()} and {@link #getSampleValues()}. The default implementation uses
   * makeEmptyMap() and calls {@link #addSampleMappings()} to add all the mappings to the map.
   */
  protected Map makeFullMap() {
    Map m = makeEmptyMap();
    addSampleMappings(m);
    return m;
  }

  @Override
  public Object makeObject() {
    return makeEmptyMap();
  }

  public void testSpecialKeysValues() {
    String[] keys = {"toString", "constructor", "__proto__", "", "null"};
    @Nullable Object[] values =
        new @Nullable Object[] {new Object(), new Object(), new Object(), new Object(), null};

    Map map = makeEmptyMap();

    assertMap(map, keys, values);

    @Nullable Object[] undefineds = new @Nullable Object[values.length];
    assertMap(map, keys, undefineds);
  }

  private void assertMap(Map map, @Nullable String[] keys, @Nullable Object[] values) {
    assertEmptyMap(map, keys, values);

    // Fill the map with special keys/values.
    for (int i = 0; i < keys.length; i++) {
      map.put(keys[i], values[i]);
    }

    // Assert the map with filled in keys/values
    for (int i = 0; i < keys.length; i++) {
      assertTrue(keys[i], map.containsKey(keys[i]));
      assertTrue(keys[i], map.containsValue(values[i]));
      assertSame(keys[i], values[i], map.get(keys[i]));
    }
    assertEquals(map.toString(), keys.length, map.size());

    // Remove the keys and assert the results
    for (int i = 0; i < keys.length; i++) {
      assertSame(keys[i], values[i], map.remove(keys[i]));
    }
    assertEmptyMap(map, keys, values);
  }

  private static void assertEmptyMap(
      Map map, final @Nullable String[] keys, final @Nullable Object[] values) {
    for (int i = 0; i < keys.length; i++) {
      assertFalse(keys[i], map.containsKey(keys[i]));
      assertFalse(keys[i], map.containsValue(values[i]));
      assertNull(keys[i], map.get(keys[i]));
    }
  }

  /**
   * Test to ensure the test setup is working properly. This method checks to ensure that the
   * getSampleKeys and getSampleValues methods are returning results that look appropriate. That is,
   * they both return a non-null array of equal length. The keys array must not have any duplicate
   * values, and may only contain a (single) null key if useNullKey() returns true. The values array
   * must only have a null value if useNullValue() is true and may only have duplicate values if
   * useDuplicateValues() returns true.
   */
  public void testSampleMappings() {
    @Nullable Object[] keys = getSampleKeys();
    @Nullable Object[] values = getSampleValues();
    Object[] newValues = getNewSampleValues();

    assertNotNull("failure in test: Must have keys returned from " + "getSampleKeys.", keys);

    assertTrue(
        "failure in test: Must have values returned from " + "getSampleValues.", values != null);

    // verify keys and values have equivalent lengths (in case getSampleX are
    // overridden)
    assertEquals(
        "failure in test: not the same number of sample " + "keys and values.",
        keys.length,
        values.length);

    assertEquals(
        "failure in test: not the same number of values and new values.",
        values.length,
        newValues.length);

    // verify there aren't duplicate keys, and check values
    for (int i = 0; i < keys.length - 1; i++) {
      for (int j = i + 1; j < keys.length; j++) {
        assertTrue("failure in test: duplicate null keys.", (keys[i] != null || keys[j] != null));
        assertTrue(
            "failure in test: duplicate non-null key.",
            (keys[i] == null
                || keys[j] == null
                || (!keys[i].equals(keys[j]) && !keys[j].equals(keys[i]))));
      }
      assertTrue(
          "failure in test: found null key, but useNullKey " + "is false.",
          keys[i] != null || useNullKey());
      assertTrue(
          "failure in test: found null value, but useNullValue " + "is false.",
          values[i] != null || useNullValue());
      assertTrue(
          "failure in test: found null new value, but useNullValue " + "is false.",
          newValues[i] != null || useNullValue());
      assertTrue(
          "failure in test: values should not be the same as new value",
          values[i] != newValues[i] && (values[i] == null || !values[i].equals(newValues[i])));
    }
  }

  // tests begin here.  Each test adds a little bit of tested functionality.
  // Many methods assume previous methods passed.  That is, they do not
  // exhaustively recheck things that have already been checked in a previous
  // test methods.

  /**
   * Test to ensure that makeEmptyMap and makeFull returns a new non-null map with each invocation.
   */
  public void testMakeMap() {
    Map em = makeEmptyMap();
    assertTrue("failure in test: makeEmptyMap must return a non-null map.", em != null);

    Map em2 = makeEmptyMap();
    assertTrue("failure in test: makeEmptyMap must return a non-null map.", em != null);

    assertTrue(
        "failure in test: makeEmptyMap must return a new map " + "with each invocation.",
        em != em2);

    Map fm = makeFullMap();
    assertTrue("failure in test: makeFullMap must return a non-null map.", fm != null);

    Map fm2 = makeFullMap();
    assertTrue("failure in test: makeFullMap must return a non-null map.", fm != null);

    assertTrue(
        "failure in test: makeFullMap must return a new map " + "with each invocation.", fm != fm2);
  }

  /** Tests Map.isEmpty() */
  public void testMapIsEmpty() {

    resetEmpty();
    assertEquals("Map.isEmpty() should return true with an empty map", true, map.isEmpty());
    verify();

    resetFull();
    assertEquals("Map.isEmpty() should return false with a non-empty map", false, map.isEmpty());
    verify();
  }

  /** Tests Map.size() */
  public void testMapSize() {
    resetEmpty();
    assertEquals("Map.size() should be 0 with an empty map", 0, map.size());
    verify();

    resetFull();
    assertEquals(
        "Map.size() should equal the number of entries " + "in the map",
        getSampleKeys().length,
        map.size());
    verify();
  }

  /**
   * Tests {@link Map#clear()}. If the map {@link #isAddRemoveModifiable() can add and remove
   * elements}, then {@link Map#size()} and {@link Map#isEmpty()} are used to ensure that map has no
   * elements after a call to clear. If the map does not support adding and removing elements, this
   * method checks to ensure clear throws an UnsupportedOperationException.
   */
  public void testMapClear() {
    if (!isAddRemoveModifiable()) {
      return;
    }

    resetEmpty();
    map.clear();
    confirmed.clear();
    verify();

    resetFull();
    map.clear();
    confirmed.clear();
    verify();
  }

  /**
   * Tests Map.containsKey(Object) by verifying it returns false for all sample keys on a map
   * created using an empty map and returns true for all sample keys returned on a full map.
   */
  public void testMapContainsKey() {
    @Nullable Object[] keys = getSampleKeys();

    resetEmpty();
    for (int i = 0; i < keys.length; i++) {
      assertTrue("Map must not contain key when map is empty", !map.containsKey(keys[i]));
    }
    verify();

    resetFull();
    for (int i = 0; i < keys.length; i++) {
      assertTrue(
          "Map must contain key for a mapping in the map. " + "Missing: " + keys[i],
          map.containsKey(keys[i]));
    }
    verify();
  }

  /**
   * Tests Map.containsValue(Object) by verifying it returns false for all sample values on an empty
   * map and returns true for all sample values on a full map.
   */
  public void testMapContainsValue() {
    Object[] values = getSampleValues();

    resetEmpty();
    for (int i = 0; i < values.length; i++) {
      assertTrue("Empty map must not contain value", !map.containsValue(values[i]));
    }
    verify();

    resetFull();
    for (int i = 0; i < values.length; i++) {
      assertTrue("Map must contain value for a mapping in the map.", map.containsValue(values[i]));
    }
    verify();
  }

  /** Tests Map.equals(Object) */
  public void testMapEquals() {
    resetEmpty();
    assertTrue("Empty maps unequal.", map.equals(confirmed));
    verify();

    resetFull();
    assertTrue("Full maps unequal.", map.equals(confirmed));
    verify();

    resetFull();
    // modify the HashMap created from the full map and make sure this
    // change results in map.equals() to return false.
    Iterator iter = confirmed.keySet().iterator();
    iter.next();
    iter.remove();
    assertTrue("Different maps equal.", !map.equals(confirmed));

    resetFull();
    assertTrue("equals(null) returned true.", !map.equals(null));
    assertTrue("equals(new Object()) returned true.", !map.equals(new Object()));
    verify();
  }

  /** Tests Map.get(Object) */
  public void testMapGet() {
    resetEmpty();

    Object[] keys = getSampleKeys();
    Object[] values = getSampleValues();

    for (int i = 0; i < keys.length; i++) {
      assertTrue("Empty map.get() should return null.", map.get(keys[i]) == null);
    }
    verify();

    resetFull();
    for (int i = 0; i < keys.length; i++) {
      assertEquals("Full map.get() should return value from mapping.", values[i], map.get(keys[i]));
    }
  }

  /** Tests Map.hashCode() */
  public void testMapHashCode() {
    resetEmpty();
    assertTrue("Empty maps have different hashCodes.", map.hashCode() == confirmed.hashCode());

    resetFull();
    assertTrue("Equal maps have different hashCodes.", map.hashCode() == confirmed.hashCode());
  }

  /**
   * Tests Map.toString(). Since the format of the string returned by the toString() method is not
   * defined in the Map interface, there is no common way to test the results of the toString()
   * method. Thereforce, it is encouraged that Map implementations override this test with one that
   * checks the format matches any format defined in its API. This default implementation just
   * verifies that the toString() method does not return null.
   */
  public void testMapToString() {
    resetEmpty();
    assertTrue("Empty map toString() should not return null", map.toString() != null);
    verify();

    resetFull();
    assertTrue("Empty map toString() should not return null", map.toString() != null);
    verify();
  }

  /** Tests Map.put(Object, Object) */
  public void testMapPut() {
    if (!isAddRemoveModifiable()) {
      return;
    }

    resetEmpty();

    Object[] keys = getSampleKeys();
    Object[] values = getSampleValues();
    Object[] newValues = getNewSampleValues();

    for (int i = 0; i < keys.length; i++) {
      Object o = map.put(keys[i], values[i]);
      confirmed.put(keys[i], values[i]);
      verify();
      assertTrue("First map.put should return null", o == null);
      assertTrue("Map should contain key after put", map.containsKey(keys[i]));
      assertTrue("Map should contain value after put", map.containsValue(values[i]));
    }

    for (int i = 0; i < keys.length; i++) {
      Object o = map.put(keys[i], newValues[i]);
      confirmed.put(keys[i], newValues[i]);
      verify();
      assertEquals("Second map.put should return previous value", values[i], o);
      assertTrue("Map should still contain key after put", map.containsKey(keys[i]));
      assertTrue("Map should contain new value after put", map.containsValue(newValues[i]));

      // if duplicates are allowed, we're not guarunteed that the value
      // no longer exists, so don't try checking that.
      if (!useDuplicateValues()) {
        assertTrue(
            "Map should not contain old value after second put", !map.containsValue(values[i]));
      }
    }
  }

  /** Tests Map.putAll(Collection) */
  public void testMapPutAll() {
    if (!isAddRemoveModifiable()) {
      return;
    }

    resetEmpty();

    Map m2 = makeFullMap();

    map.putAll(m2);
    confirmed.putAll(m2);
    verify();

    resetEmpty();

    m2 = new HashMap();
    Object[] keys = getSampleKeys();
    Object[] values = getSampleValues();
    for (int i = 0; i < keys.length; i++) {
      m2.put(keys[i], values[i]);
    }

    map.putAll(m2);
    confirmed.putAll(m2);
    verify();
  }

  /** Tests Map.remove(Object) */
  public void testMapRemove() {
    if (!isAddRemoveModifiable()) {
      return;
    }

    resetEmpty();

    Object[] keys = getSampleKeys();
    Object[] values = getSampleValues();
    for (int i = 0; i < keys.length; i++) {
      Object o = map.remove(keys[i]);
      assertTrue("First map.remove should return null", o == null);
    }
    verify();

    resetFull();

    for (int i = 0; i < keys.length; i++) {
      Object o = map.remove(keys[i]);
      confirmed.remove(keys[i]);
      verify();

      assertEquals("map.remove with valid key should return value", values[i], o);
    }

    Object[] other = getOtherKeys();

    resetFull();
    int size = map.size();
    for (int i = 0; i < other.length; i++) {
      Object o = map.remove(other[i]);
      assertEquals("map.remove for nonexistent key should return null", o, null);
      assertEquals("map.remove for nonexistent key should not " + "shrink map", size, map.size());
    }
    verify();
  }

  public void testFailFastEntrySet() {
    if (!isAddRemoveModifiable()) {
      return;
    }
    if (!isFailFastExpected()) {
      return;
    }
    resetFull();
    Iterator<Map.Entry<@Nullable Object, @Nullable Object>> it = map.entrySet().iterator();
    final Map.Entry val = it.next();
    map.remove(val.getKey());
    try {
      it.next();
      fail();
    } catch (ConcurrentModificationException expected) {
    }

    resetFull();
    it = map.entrySet().iterator();
    it.next();
    map.clear();
    try {
      it.next();
      fail();
    } catch (ConcurrentModificationException expected) {
    }
  }

  public void testFailFastKeySet() {
    if (!isAddRemoveModifiable()) {
      return;
    }
    if (!isFailFastExpected()) {
      return;
    }
    resetFull();
    Iterator it = map.keySet().iterator();
    final Object val = it.next();
    map.remove(val);
    try {
      it.next();
      fail();
    } catch (ConcurrentModificationException expected) {
    }

    resetFull();
    it = map.keySet().iterator();
    it.next();
    map.clear();
    try {
      it.next();
      fail();
    } catch (ConcurrentModificationException expected) {
    }
  }

  public void testFailFastValues() {
    if (!isAddRemoveModifiable()) {
      return;
    }
    if (!isFailFastExpected()) {
      return;
    }
    resetFull();
    Iterator it = map.values().iterator();
    it.next();
    map.remove(map.keySet().iterator().next());
    try {
      it.next();
      fail();
    } catch (ConcurrentModificationException expected) {
    }

    resetFull();
    it = map.values().iterator();
    it.next();
    map.clear();
    try {
      it.next();
      fail();
    } catch (ConcurrentModificationException expected) {
    }
  }

  /**
   * Utility methods to create an array of Map.Entry objects out of the given key and value arrays.
   *
   * <p>
   *
   * @param keys the array of keys
   * @param values the array of values
   * @return an array of Map.Entry of those keys to those values
   */
  private Map.Entry[] makeEntryArray(Object[] keys, Object[] values) {
    Map.Entry[] result = new Map.Entry[keys.length];
    for (int i = 0; i < keys.length; i++) {
      result[i] = new DefaultMapEntry(keys[i], values[i]);
    }
    return result;
  }

  class TestMapEntrySet extends TestSet {
    public TestMapEntrySet() {
      super("");
    }

    // Have to implement manually; entrySet doesn't support addAll
    @Override
    protected @Nullable Object[] getFullElements() {
      Object[] k = getSampleKeys();
      Object[] v = getSampleValues();
      return makeEntryArray(k, v);
    }

    // Have to implement manually; entrySet doesn't support addAll
    @Override
    protected Object[] getOtherElements() {
      Object[] k = getOtherKeys();
      Object[] v = getOtherValues();
      return makeEntryArray(k, v);
    }

    @Override
    protected Set<@Nullable Object> makeEmptySet() {
      Object result = makeEmptyMap().entrySet();
      return (Set<@Nullable Object>) result;
    }

    @Override
    protected Set<@Nullable Object> makeFullSet() {
      Object result = makeFullMap().entrySet();
      return (Set<@Nullable Object>) result;
    }

    @Override
    protected boolean isAddSupported() {
      // Collection views don't support add operations.
      return false;
    }

    @Override
    protected boolean isRemoveSupported() {
      // Entry set should only support remove if map does
      return isAddRemoveModifiable();
    }

    @Override
    protected void resetFull() {
      TestMap.this.resetFull();
      collection = map.entrySet();
      TestMapEntrySet.this.confirmed = TestMap.this.confirmed.entrySet();
    }

    @Override
    protected void resetEmpty() {
      TestMap.this.resetEmpty();
      collection = map.entrySet();
      TestMapEntrySet.this.confirmed = TestMap.this.confirmed.entrySet();
    }

    @Override
    protected void verify() {
      super.verify();
      TestMap.this.verify();
    }
  }

  class TestMapKeySet extends TestSet {
    public TestMapKeySet() {
      super("");
    }

    @Override
    protected @Nullable Object[] getFullElements() {
      return getSampleKeys();
    }

    @Override
    protected Object[] getOtherElements() {
      return getOtherKeys();
    }

    @Override
    protected Set makeEmptySet() {
      return makeEmptyMap().keySet();
    }

    @Override
    protected Set makeFullSet() {
      return makeFullMap().keySet();
    }

    @Override
    protected boolean isAddSupported() {
      return false;
    }

    @Override
    protected boolean isRemoveSupported() {
      return isAddRemoveModifiable();
    }

    @Override
    protected void resetEmpty() {
      TestMap.this.resetEmpty();
      collection = map.keySet();
      TestMapKeySet.this.confirmed = TestMap.this.confirmed.keySet();
    }

    @Override
    protected void resetFull() {
      TestMap.this.resetFull();
      collection = map.keySet();
      TestMapKeySet.this.confirmed = TestMap.this.confirmed.keySet();
    }

    @Override
    protected void verify() {
      super.verify();
      TestMap.this.verify();
    }
  }

  class TestMapValues extends TestCollection {
    public TestMapValues() {}

    @Override
    protected @Nullable Object[] getFullElements() {
      return getSampleValues();
    }

    @Override
    protected Object[] getOtherElements() {
      return getOtherValues();
    }

    @Override
    protected Collection makeCollection() {
      return makeEmptyMap().values();
    }

    @Override
    protected Collection makeFullCollection() {
      return makeFullMap().values();
    }

    @Override
    protected boolean isAddSupported() {
      return false;
    }

    @Override
    protected boolean isRemoveSupported() {
      return isAddRemoveModifiable();
    }

    @Override
    protected boolean areEqualElementsDistinguishable() {
      // equal values are associated with different keys, so they are
      // distinguishable.
      return true;
    }

    @Override
    protected Collection makeConfirmedCollection() {
      // never gets called, reset methods are overridden
      return null;
    }

    @Override
    protected Collection makeConfirmedFullCollection() {
      // never gets called, reset methods are overridden
      return null;
    }

    @Override
    protected void resetFull() {
      TestMap.this.resetFull();
      collection = map.values();
      TestMapValues.this.confirmed = TestMap.this.confirmed.values();
    }

    @Override
    protected void resetEmpty() {
      TestMap.this.resetEmpty();
      collection = map.values();
      TestMapValues.this.confirmed = TestMap.this.confirmed.values();
    }

    @Override
    protected void verify() {
      super.verify();
      TestMap.this.verify();
    }

    // TODO: should test that a remove on the values collection view
    // removes the proper mapping and not just any mapping that may have
    // the value equal to the value returned from the values iterator.
  }

  /**
   * Resets the {@link #map}, {@link #entrySet}, {@link #keySet}, {@link #collectionValues} and
   * {@link #confirmed} fields to empty.
   */
  protected void resetEmpty() {
    this.map = makeEmptyMap();
    views();
    this.confirmed = makeConfirmedMap();
  }

  /**
   * Resets the {@link #map}, {@link #entrySet}, {@link #keySet}, {@link #collectionValues} and
   * {@link #confirmed} fields to full.
   */
  protected void resetFull() {
    this.map = makeFullMap();
    views();
    this.confirmed = makeConfirmedMap();
    Object[] k = getSampleKeys();
    Object[] v = getSampleValues();
    for (int i = 0; i < k.length; i++) {
      confirmed.put(k[i], v[i]);
    }
  }

  /** Resets the collection view fields. */
  private void views() {
    this.keySet = map.keySet();
    this.collectionValues = map.values();
    this.entrySet = map.entrySet();
  }

  /**
   * Verifies that {@link #map} is still equal to {@link #confirmed}. This method checks that the
   * map is equal to the HashMap, <I>and</I> that the map's collection views are still equal to the
   * HashMap's collection views. An <Code>equals</Code> test is done on the maps and their
   * collection views; their size and <Code>isEmpty</Code> results are compared; their hashCodes are
   * compared; and <Code>containsAll</Code> tests are run on the collection views.
   */
  protected void verify() {
    verifyMap();
    verifyEntrySet();
    verifyKeySet();
  }

  protected void verifyMap() {
    int size = confirmed.size();
    boolean empty = confirmed.isEmpty();
    assertEquals("Map should be same size as HashMap", size, map.size());
    assertEquals("Map should be empty if HashMap is", empty, map.isEmpty());
    assertEquals("hashCodes should be the same", confirmed.hashCode(), map.hashCode());
    // this fails for LRUMap because confirmed.equals() somehow modifies
    // map, causing concurrent modification exceptions.
    // assertEquals("Map should still equal HashMap", confirmed, map);
    // this works though and performs the same verification:
    assertTrue("Map should still equal HashMap", map.equals(confirmed));
    // TODO: this should really be rexamined to figure out why LRU map
    // behaves like it does (the equals shouldn't modify since all accesses
    // by the confirmed collection should be through an iterator, thus not
    // causing LRUMap to change).
  }

  protected void verifyEntrySet() {
    int size = confirmed.size();
    boolean empty = confirmed.isEmpty();
    assertEquals("entrySet should be same size as HashMap's", size, entrySet.size());
    assertEquals("entrySet should be empty if HashMap is", empty, entrySet.isEmpty());
    assertTrue(
        "entrySet should contain all HashMap's elements",
        entrySet.containsAll(confirmed.entrySet()));
    assertEquals(
        "entrySet hashCodes should be the same",
        confirmed.entrySet().hashCode(),
        entrySet.hashCode());
    assertEquals("Map's entry set should still equal HashMap's", confirmed.entrySet(), entrySet);
  }

  protected void verifyKeySet() {
    int size = confirmed.size();
    boolean empty = confirmed.isEmpty();
    assertEquals("keySet should be same size as HashMap's", size, keySet.size());
    assertEquals("keySet should be empty if HashMap is", empty, keySet.isEmpty());
    assertTrue(
        "keySet should contain all HashMap's elements", keySet.containsAll(confirmed.keySet()));
    assertEquals(
        "keySet hashCodes should be the same", confirmed.keySet().hashCode(), keySet.hashCode());
    assertEquals("Map's key set should still equal HashMap's", confirmed.keySet(), keySet);
  }
}
